using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

using CellarDoor.Gui.Elements;
using CellarDoor.Gui.Theming;
using CellarDoor.Gui.Layout;

namespace CellarDoor.Gui
{
    /// <summary>
    /// A <c>DrawableGameComponent</c> that is able to instruct every holded 
    /// <c>GuiLayer</c> to update and draw itself. Further it keeps track of
    /// the used screenresolution. When resolution changes the manager 
    /// automaticly calculates the factor between the default and the new 
    /// resolution and rises the ScaleFactorChanged event.
    /// </summary>
    public class UiManager : DrawableGameComponent
    {
        private Viewport nativeViewport;
        /// <summary>
        /// Gets and sets the native viewport. This should be the viewport definition
        /// for that the screens have been designed for.
        /// </summary>
        public Viewport NativeViewport
        {
            get { return nativeViewport; }
            set 
            {
                nativeViewport = value;
                OnViewportChanged();
            }
        }

        private Viewport targetViewport;
        /// <summary>
        /// Gets and sets the viewport where the ui elements are rendered to.
        /// </summary>
        public Viewport Viewport
        {
            get { return targetViewport; }
            set
            {
                targetViewport = value;
                OnViewportChanged();
            }
        }

        private Vector2 elementScale = Vector2.One;
        /// <summary>
        /// Gets the scaling value that is used to scale the ui elements from native to target resolution
        /// </summary>
        public Vector2 ElementScale
        {
            get { return elementScale; }
        }

        private UiElement elementInFocus;
        /// <summary>
        /// Gets and sets the focused control
        /// </summary>
        public UiElement ElementInFocus
        {
            get { return elementInFocus; }
            set 
            {
                elementInFocus = value;
                FrameInFocus = GetFrame(value, true);
            }
        }

        private UiElement frameInFocus;
        /// <summary>
        /// Gets and sets the focused control
        /// </summary>
        public UiElement FrameInFocus
        {
            get { return frameInFocus; }
            set 
            {
                if (frameInFocus != value)
                {
                    frameInFocus = value;
                    if (value != null && elements.Contains(value))
                    {
                        elements.Remove(value);
                        elements.Add(value);
                    }
                }
            }
        }

        /// <summary>
        /// Gets and sets the default ui theme
        /// </summary>
        public UiTheme Theme { get; set; }

        public bool CursorEnable { get; set; }

        public bool DebugEnable { get; set; }

        private UiThemeSprite cursorSprite;

        private string cursor;
        /// <summary>
        /// 
        /// </summary>
        public string Cursor 
        {
            get { return cursor; }
            set
            {
                if (cursor != value)
                {
                    cursor = value;
                    cursorSprite = Theme.GetSprite(value, Viewport.Width, Viewport.Height);
                }
            }
        }

        /// <summary>
        /// Occurs when the viewport has changed
        /// </summary>
        public event EventHandler ViewportChanged;

        private List<UiElement> elements = new List<UiElement>();

        private List<UiElement> elementsToUpdate = new List<UiElement>();

        private bool inDraw = false;

        private Stack<Rectangle> scissorStack = new Stack<Rectangle>();

        protected RasterizerState RasterizerState;

        protected SpriteBatch SpriteBatch;

        protected UiMouseProxy Mouse = new UiMouseProxy();

        private MouseState mouseNew;

        private MouseState mouseOld;

        private Texture2D textureWhite;

        #region Constructors
        /// <summary>
        /// Creates a GuiManager
        /// </summary>
        /// <param name="game"></param>
        /// <param name="referenceResolution"></param>
        /// <param name="addToComponents"></param>
        public UiManager(Game game)
            : base(game)
        {
            nativeViewport.Width = 800;
            nativeViewport.Height = 600;
            targetViewport.Width = 800;
            targetViewport.Height = 600;
        }
        #endregion

        protected override void LoadContent()
        {
            base.LoadContent();

            RasterizerState = new RasterizerState();
            RasterizerState.CullMode = CullMode.CullCounterClockwiseFace;
            RasterizerState.FillMode = FillMode.Solid;
            RasterizerState.ScissorTestEnable = true;
            SpriteBatch = new SpriteBatch(GraphicsDevice);

            textureWhite = new Texture2D(GraphicsDevice, 2, 2);
            textureWhite.SetData<Color>(new Color[] { Color.White, Color.White, Color.White, Color.White });
        }
        
        #region Update
        /// <summary>
        /// Manager logic is updated here. Removes old and adds new layers.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Update(GameTime gameTime)
        {
            UpdateMouse(Mouse);
            UpdateElements(gameTime, Mouse);

            base.Update(gameTime);
        }

        private void UpdateElements(GameTime time, UiMouseProxy mouse)
        {
            if (mouse.ButtonLeftJustPressed)
            {
                elementInFocus = GetElementFromPoint(mouse.PositionNew, false);
            }

            // update the layers
            elementsToUpdate.Clear();
            elementsToUpdate.AddRange(elements);
            for (int i = 0; i < elementsToUpdate.Count; i++)
            {
                elementsToUpdate[i].Update(time);
                elementsToUpdate[i].UpdateState(time, mouse);
            }
            elementsToUpdate.Clear();
        }

        protected virtual void UpdateMouse(UiMouseProxy mouse)
        {
            mouseOld = mouseNew;
            mouseNew = Microsoft.Xna.Framework.Input.Mouse.GetState();

            mouse.ButtonLeftJustPressed = 
                (mouseOld.LeftButton == ButtonState.Released) && 
                (mouseNew.LeftButton == ButtonState.Pressed);
            mouse.ButtonLeftPressed = mouseNew.LeftButton == ButtonState.Pressed;
            mouse.ButtonLeftJustReleased = 
                (mouseOld.LeftButton == ButtonState.Pressed) && 
                (mouseNew.LeftButton == ButtonState.Released);

            mouse.ButtonRightJustPressed = 
                (mouseOld.RightButton == ButtonState.Released) && 
                (mouseNew.RightButton == ButtonState.Pressed);
            mouse.ButtonRightPressed = mouseNew.RightButton == ButtonState.Pressed;
            mouse.ButtonRightJustReleased = 
                (mouseOld.RightButton == ButtonState.Pressed) && 
                (mouseNew.RightButton == ButtonState.Released);

            mouse.PositionNew = new Point(mouseNew.X, mouseNew.Y);
            mouse.PositionOld = new Point(mouseOld.X, mouseOld.Y);
            mouse.PositionDelta = new Point(mouseNew.X - mouseOld.X, mouseNew.Y - mouseOld.Y);

            mouse.WheelDirection = mouseNew.ScrollWheelValue - mouseOld.ScrollWheelValue;
        }

        #endregion

        /// <summary>
        /// Adds a layer to the manager.
        /// </summary>
        /// <param name="screen"></param>
        public void AddScreen(UiElement layer)
        {
            if (!this.Equals(layer.Manager))
            {
                throw new UiException(
                    "Given element ist initialized with a different manager.");
            }
            if (!elements.Contains(layer))
            {
                elements.Add(layer);
            }
        }

        /// <summary>
        /// Removes a layer from the manager
        /// </summary>
        /// <param name="screen"></param>
        public void RemoveScreen(UiElement layer)
        {
            elements.Remove(layer);
        }

        private void OnViewportChanged()
        {
            elementScale.X = (float)targetViewport.Width / (float)nativeViewport.Width;
            elementScale.Y = (float)targetViewport.Height / (float)nativeViewport.Height;

            if (ViewportChanged != null)
            {
                ViewportChanged(this, EventArgs.Empty);
            }
        }

        public UiFrame GetFrame(UiElement element, bool managerLevelOnly)
        {
            if (element == null)
            {
                return null;
            }

            UiFrame frame = element as UiFrame;
            if (frame != null)
            {
                if (managerLevelOnly)
                {
                    if (elements.Contains(frame))
                    {
                        return frame;
                    }
                }
                else
                {
                    return frame;
                }
            }

            return GetFrame(element.Parent, managerLevelOnly);
        }

        /// <summary>
        /// Gets an element from mouseposition.
        /// </summary>
        /// <param name="point">The mouse position on screen</param>
        /// <param name="native">Value indicating whether the native or the target viwport to use</param>
        /// <returns></returns>
        public UiElement GetElementFromPoint(Point point, bool native)
        {
            UiElement control = null;
            for (int i = 0; i < elements.Count; i++)
            {
                UiElement temp = elements[i].GetElementFromScreenPoint(point, native);
                if (temp != null)
                {
                    control = temp;
                }
            }
            return control;
        }

        #region Draw
        /// <summary>
        /// Tell each layer in update list to draw itself.
        /// Also tells the cursor to draw itself if initialized.
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Draw(GameTime gameTime)
        {
            if (Theme == null)
            {
                throw new UiException("No theme was loaded to UI manager. Please load a theme before drawing th UI");
            }

            inDraw = true;

            SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState);
            for (int i = 0; i < elements.Count; i++)
            {
                elements[i].Draw(Vector4.One);
            }

            if (CursorEnable && cursorSprite != null && cursorSprite.Texture != null)
            {
                DrawSprite(cursorSprite, Mouse.PositionNew, Color.White);
            }

            if (DebugEnable)
            {
                var el = GetElementFromPoint(Mouse.PositionNew, false);
                if (el != null)
                {
                    var target = el.Layout.AreaScaled;
                    //DebugRectangle(target, Color.White, 0.25f);
                    DrawRectangles(textureWhite, ResolvePaddingRectangle(target, el.Layout.PaddingScaled), Color.DarkViolet, 0.25f);
                    DrawRectangles(textureWhite, ResolveMarginRectangle(target, el.Layout.MarginScaled), Color.DarkGoldenrod, 0.25f);
                }
            }

            SpriteBatch.End();

            if (scissorStack.Count != 0)
            {
                throw new UiException("EndScissor must be called after each BeginScissor call.");
            }

            inDraw = false;
            base.Draw(gameTime);
        }

        /// <summary>
        /// Pushes a new scissor rectangle. 
        /// </summary>
        /// <param name="scissor"></param>
        public void BeginScissor(Rectangle scissor)
        {
            if (!inDraw)
            {
                throw new UiException("Draw must be called before BeginScissor.");
            }

            SpriteBatch.End();

            scissorStack.Push(Rectangle.Intersect(GraphicsDevice.ScissorRectangle, scissor));
            GraphicsDevice.ScissorRectangle = scissorStack.Peek();

            SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState);
        }

        /// <summary>
        /// Pops the last scissor rectangle
        /// </summary>
        public void EndScissor()
        {
            if (!inDraw || scissorStack.Count == 0)
            {
                throw new UiException("BeginScissor must be called before EndScissor.");
            }

            SpriteBatch.End();

            scissorStack.Pop();
            if (scissorStack.Count > 0)
            {
                GraphicsDevice.ScissorRectangle = scissorStack.Peek();
            }
            else
            {
                GraphicsDevice.ScissorRectangle = GraphicsDevice.Viewport.Bounds;
            }

            SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
        }


        /// <summary>
        /// Draws a single sprite into the target rectangle on screen with given color
        /// </summary>
        /// <param name="sprite"></param>
        /// <param name="target"></param>
        /// <param name="color"></param>
        public void DrawSprite(UiThemeSprite sprite, UiRectangle target, Color color)
        {
            if (sprite.Texture != null)
            {
                for (int i = 0; i < target.Length; i++)
                {
                    SpriteBatch.Draw(sprite.Texture, target[i], sprite.Rectangle[i], color);
                }
            }
        }

        public void DrawSprite(UiThemeSprite sprite, Point position, Color color)
        {
            UiRectangle target = sprite.Rectangle;
            target.SetPosition(position.X, position.Y);

            if (sprite.Texture != null)
            {
                for (int i = 0; i < target.Length; i++)
                {
                    SpriteBatch.Draw(sprite.Texture, target[i], sprite.Rectangle[i], color);
                }
            }
        }

        /// <summary>
        /// Draws a texture into a rectangle in screen
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="target"></param>
        /// <param name="source"></param>
        /// <param name="color"></param>
        public void DrawRectangle(Texture2D texture, UiRectangle target, UiRectangle source, Color color)
        {
            for (int i = 0; i < target.Length; i++)
            {
                SpriteBatch.Draw(texture, target[i], source[i], color);
            }
        }

        /// <summary>
        /// Draws a texture into a rectangle on screen
        /// </summary>
        /// <param name="texture"></param>
        /// <param name="target"></param>
        /// <param name="source"></param>
        /// <param name="color"></param>
        public void DrawRectangle(Texture2D texture, Rectangle target, Rectangle source, Color color)
        {
            SpriteBatch.Draw(texture, target, source, color);
        }

        public void DrawRectangle(Texture2D texture, Rectangle target, Color color)
        {
            SpriteBatch.Draw(texture, target, null, color);
        }

        public void DrawRectangles(Texture2D texture, IEnumerable<Rectangle> targets, Color color, float opacity)
        {
            color.A = (byte)(opacity * byte.MaxValue);
            foreach (var item in targets)
            {
                SpriteBatch.Draw(texture, item, null, color);
            }
        }

        /// <summary>
        /// Draws a string
        /// </summary>
        /// <param name="font"></param>
        /// <param name="text"></param>
        /// <param name="position"></param>
        /// <param name="color"></param>
        /// <param name="scale"></param>
        /// <param name="rotation"></param>
        public void DrawString(SpriteFont font, string text, Vector2 position, Color color, float scale, float rotation)
        {
            position = new Vector2((int)position.X, (int)position.Y);
            SpriteBatch.DrawString(font, text, position, color, rotation, Vector2.Zero, scale, SpriteEffects.None, 0);
        }

        public void DebugRectangle(UiRectangle rectangle, Color color, float opacity)
        {
            Color a = new Color(color.ToVector3() * 0.25f);
            Color b = new Color(color.ToVector3() * 0.75f);
            for (int i = 0; i < rectangle.Length; i++)
            {
                Color c = i % 2 == 0 ? a : b;
                c.A = (byte)(opacity * byte.MaxValue);
                SpriteBatch.Draw(textureWhite, rectangle[i], c);
            }
        }

        private Rectangle[] ResolvePaddingRectangle(Rectangle r, UiPadding p)
        {
            Rectangle[] rects = new Rectangle[4];

            // top
            rects[0] = new Rectangle(r.X, r.Y, r.Width, p.Top);
            // bottom
            rects[1] = new Rectangle(r.X, r.Bottom - p.Bottom, r.Width, p.Bottom);
            // left
            rects[2] = new Rectangle(r.X, r.Y + p.Top, p.Left, r.Height - p.Top - p.Bottom);
            // right
            rects[3] = new Rectangle(r.Right - p.Right, r.Y + p.Top, p.Right, r.Height - p.Top - p.Bottom);

            return rects;
        }

        private Rectangle[] ResolveMarginRectangle(Rectangle r, UiPadding p)
        {
            r.X -= p.Left;
            r.Y -= p.Top;
            r.Width += (p.Left + p.Right);
            r.Height += (p.Top + p.Bottom);
            return ResolvePaddingRectangle(r, p);
        }
        #endregion
    }
}
